package com.yifeng.repo.micro.service.server.dao;

import com.alibaba.druid.DruidRuntimeException;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.util.Base64;
import com.alibaba.druid.util.JdbcUtils;
import com.google.common.base.Splitter;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.Closeable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author maoyun
 */
@Slf4j
public class DruidDataSourceManager implements Closeable {

    /**
     * key, dbId
     */
    private static final Map<String, DruidDataSource> POOL = new ConcurrentHashMap<>();


    /**
     * 获取数据源操作对象. 数据源修改后, 会根据dbId更新对应的JdbcTemplate, 尽量将这个对象限制在局部变量中
     * @return 数据源操作对象
     */
    public static DruidDataSource getDataSource(Config config) {
        String dbId = getId(config);
        DruidDataSource druidDataSource = POOL.get(dbId);
        if (druidDataSource == null) {
            try {
                createDataSource(config);
            } catch (Exception e) {
                throw new DruidRuntimeException(e);
            }
            druidDataSource = POOL.get(dbId);
            if (druidDataSource == null) {
                throw new DruidRuntimeException("数据源初始化失败" + config);
            }
        }
        return druidDataSource;
    }

    /**
     * ip+port+dbType+username 确定唯一库
     * jdbc:mysql://192.168.254.37:59003/yfcloud_example
     * jdbc:oracle:thin:@192.168.1.213:1521:YF03
     * jdbc:clickhouse://xxxx:port/database
     *
     * @param config 数据库链接配置
     * @return String
     */
    public static String getId(Config config) {
        String jdbcUrl = config.getJdbcUrl();
        String username = config.getUsername();
        DbType dbType = getDbTypeByUrl(jdbcUrl);

        String removePrefixUrl = jdbcUrl.replace(dbType.getProtocol(), "");
        List<String> paramList = Splitter.on(":").splitToList(removePrefixUrl);
        String ip;
        String port;
        if (dbType == DbType.ORACLE) {
            ip = paramList.get(0);
            port = paramList.get(1);
        } else {
            ip = paramList.get(0);
            port = paramList.get(1).split(DbConsts.SINGLE_SLASH_RIGHT)[0];
        }
        // TODO 可能出现同一个实例不同库使用同一个帐号的情况？？
        String joinKey = String.join("_", ip, port, dbType.name(), username);

        log.info("生成dbId原文:{}", joinKey);
        String dbId = Base64.byteArrayToBase64(joinKey.getBytes(StandardCharsets.UTF_8));
        config.setId(dbId);
        log.info("生成dbId密文:{}", dbId);
        return dbId;
    }

    private static DbType getDbTypeByUrl(String jdbcUrl) {
        if (jdbcUrl.startsWith(DbConsts.MYSQL_HEAD)) {
            return DbType.MYSQL;
        }
        if (jdbcUrl.startsWith(DbConsts.ORACLE_HEAD)) {
            return DbType.ORACLE;
        }
        return DbType.CLICK_HOUSE;
    }

    @Override
    public void close() {
        for (DruidDataSource dataSource : POOL.values()) {
            JdbcUtils.close(dataSource);
        }
        POOL.clear();
    }

    private void removeByConfig(Config config){
        String dbId = getId(config);
        removeByDbId(dbId);
    }

    private synchronized void removeByDbId(String dbId){
        DruidDataSource ds = POOL.get(dbId);
        JdbcUtils.close(ds);
        POOL.remove(dbId);
    }

    /**
     * 注意: 这里不要执行初始化连接的操作, 避免长时间卡住 导致节点启动缓慢
     */
    private static void createDataSource(Config config) {
        POOL.compute(config.getId(), (dsId, v) -> doCreateJdbcOperate(config, v));
    }

    /**
     * 创建DruidDataSource
     * @param config 连接配置
     * @param oldDruidDataSource 对比老连接变更后重置
     * @return DruidDataSource
     */
    private static synchronized DruidDataSource doCreateJdbcOperate(Config config , DruidDataSource oldDruidDataSource){
        String jdbcUrl = config.getJdbcUrl();
        String username = config.getUsername();
        String password = config.getPassword();
        if (oldDruidDataSource != null) {
            if (oldDruidDataSource.getUrl().equals(jdbcUrl) && oldDruidDataSource.getUsername().equals(username) && oldDruidDataSource.getPassword().equals(password)) {
                return oldDruidDataSource;
            }
            JdbcUtils.close(oldDruidDataSource);
        }

        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(jdbcUrl);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setInitialSize(5);
        ds.setMinIdle(1);
        ds.setMaxActive(config.getPoolSize());
        ds.setMaxWait(6000);
        if (config.getJdbcUrl().contains(DbConsts.CLICK_HOUSE_HEAD)) {
            ds.setValidationQuery("SELECT 1");
            ds.setDbType(com.alibaba.druid.DbType.clickhouse);
        } else {
            if (config.getJdbcUrl().contains(DbConsts.MYSQL_HEAD)){
                ds.setDbType(com.alibaba.druid.DbType.mysql);
            } else {
                ds.setDbType(com.alibaba.druid.DbType.oracle);
            }
            ds.setValidationQuery("SELECT 1 FROM DUAL");
        }
        ds.setKeepAlive(true);
        // 针对分布式数据库优化的，通过达到一定使用次数后断开重连，使得多个服务器间负载更均衡
        ds.setPhyMaxUseCount(1000);
        return ds;
    }

    @Data
    @NoArgsConstructor
    public static class Config {
        /**
         * dbId 内部使用,可不填写
         */
        private String id;
        private String jdbcUrl;
        private String username;
        private String password;
        private int poolSize = 5;

        public Config(String jdbcUrl, String username, String password, int poolSize) {
            this.jdbcUrl = jdbcUrl;
            this.username = username;
            this.password = password;
            this.poolSize = poolSize;
        }

        @Override
        public String toString() {
            return "DataSourceConfig{" +
                    "id='" + id + '\'' +
                    ", jdbcUrl='" + jdbcUrl + '\'' +
                    ", username='" + username + '\'' +
                    '}';
        }
    }

    static class DbConsts {
        public static final String MYSQL_HEAD = "jdbc:mysql://";
        public static final String SINGLE_SLASH_RIGHT = "/";
        public static final String ORACLE_HEAD = "jdbc:oracle:thin:@";
        /** 4.9.0  modify by lx
         for:  新增clickhouse 数据类型
         **/
        public static final String CLICK_HOUSE_HEAD = "jdbc:clickhouse://";
    }

    enum DbType {
        //
        MYSQL('`', "jdbc:mysql://"),
        ORACLE('"', "jdbc:oracle:thin:@"),
        CLICK_HOUSE('`', "jdbc:clickhouse://");
        public final char escape;
        private final String protocol;


        DbType(char escape, String protocol) {
            this.escape = escape;
            this.protocol = protocol;
        }

        public String getProtocol() {
            return protocol;
        }
    }
}
